You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

664 lines
21 KiB

"use client";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import {
hasQuestionAnswerValue,
QuestionAnswersProvider,
useQuestionAnswers,
} from "@/components/questions/question-answer-storage";
import QuestionButton from "@/components/questions/question-button";
import { QuestionCheckbox } from "@/components/questions/question-checkbox";
import QuestionDate from "@/components/questions/question-date";
import QuestionDropdown from "@/components/questions/question-dropdown";
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
import QuestionFile from "@/components/questions/question-file";
import QuestionNumber from "@/components/questions/question-number";
import QuestionPhone from "@/components/questions/question-phone";
import QuestionPhoto from "@/components/questions/question-photo";
import QuestionRadio from "@/components/questions/question-radio";
import QuestionSectionFlow from "@/components/questions/question-section-flow";
import QuestionSlider from "@/components/questions/question-slider";
import QuestionText from "@/components/questions/question-text";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
import {
getQuestionListItemBySlug,
isQuestionListItemVisibleForProfile,
isQuestionRequiredForProfile,
isQuestionVisibleForProfile,
type QuestionField,
} from "@/data/question-data";
import type { MarriageGender } from "@/hooks/marriage/types";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { defaultLocale, type Locale } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
import TestIntroPage from "@/components/questions/test-intro-page";
import TestQuestionsFlow, { type TestQuestion } from "@/components/questions/test-questions-flow";
import AnswerPaceSheet from "./answer-pace-sheet";
type QuestionDetailClientProps = {
closeLabel: string;
continueLabel: string;
description: string;
informationLabel: string;
itemSlug: string;
locale?: Locale;
questionsListHref: string;
title: string;
};
type StoredQuestionField = {
label?: string;
value?: unknown;
};
type StoredAnswers = {
fields?: StoredQuestionField[];
};
function getQuestionStorageKey(slug: string) {
return `marriage:sections:${slug}:answers`;
}
function parseStoredAge(value: unknown) {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const trimmedValue = value.trim();
if (!trimmedValue) {
return null;
}
const numericAge = Number(trimmedValue);
if (Number.isFinite(numericAge)) {
return numericAge;
}
const dateOfBirth = new Date(trimmedValue);
if (Number.isNaN(dateOfBirth.getTime())) {
return null;
}
const today = new Date();
let age = today.getFullYear() - dateOfBirth.getFullYear();
const hasBirthdayPassed =
today.getMonth() > dateOfBirth.getMonth() ||
(today.getMonth() === dateOfBirth.getMonth() &&
today.getDate() >= dateOfBirth.getDate());
if (!hasBirthdayPassed) {
age -= 1;
}
return age >= 0 ? age : null;
}
return null;
}
function getStoredAge() {
try {
const rawValue = window.localStorage.getItem(
getQuestionStorageKey("personal_info"),
);
if (!rawValue) {
return null;
}
const storedAnswers = JSON.parse(rawValue) as StoredAnswers;
const ageField = storedAnswers.fields?.find(
(field) => field.label === "Age",
);
if (ageField) {
return parseStoredAge(ageField.value);
}
const dateOfBirthField = storedAnswers.fields?.find(
(field) => field.label === "Date of Birth",
);
return parseStoredAge(dateOfBirthField?.value);
} catch {
return null;
}
}
function renderQuestion(
question: QuestionField,
questionIndex: number,
disabled?: boolean,
dobQuestion?: QuestionField,
dobQuestionIndex?: number,
) {
const compactTextHeight =
question.type === "text" &&
(question.title.toLowerCase().includes("name") ||
question.title.toLowerCase().includes("email") ||
question.title.toLowerCase().includes("city") ||
question.title.toLowerCase().includes("residence") ||
question.title.toLowerCase().includes("location") ||
question.title.toLowerCase().includes("description") ||
question.title.toLowerCase().includes("short") ||
question.title.toLowerCase().includes("duration") ||
question.title.toLowerCase().includes("reason") ||
question.title.toLowerCase().includes("lifestyle") ||
question.title.toLowerCase().includes("marja") ||
question.title.toLowerCase().includes("range") ||
question.title.toLowerCase().includes("ethnicity") ||
question.title.toLowerCase().includes("nationality") ||
question.title.toLowerCase().includes("مدت") ||
question.title.toLowerCase().includes("علت") ||
question.title.toLowerCase().includes("بازه") ||
question.title.toLowerCase().includes("قومیت") ||
question.title.toLowerCase().includes("ملیت") ||
question.title.toLowerCase().includes("کوتاه") ||
question.title.toLowerCase().includes("سبک زندگی") ||
question.title.toLowerCase().includes("مرجع")) &&
!question.title.toLowerCase().includes("detailed") &&
!question.title.toLowerCase().includes("biography")
? "h-[54px] min-h-0 py-2"
: undefined;
switch (question.type) {
case "button":
return (
<QuestionButton
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "checkbox":
return (
<QuestionCheckbox
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "date":
return (
<QuestionDate
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "dropdown":
return (
<QuestionDropdown
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "file":
return (
<QuestionFile
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "number":
if (
question.title === "Age" &&
dobQuestion &&
dobQuestionIndex !== undefined
) {
return (
<QuestionNumber
question={question}
questionIndex={questionIndex}
derivedFromQuestion={dobQuestion}
derivedFromQuestionIndex={dobQuestionIndex}
disabled={disabled}
/>
);
}
return (
<QuestionNumber
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "phone":
return (
<QuestionPhone
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "photo":
return (
<QuestionPhoto
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "radio":
return (
<QuestionRadio
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "slider":
return (
<QuestionSlider
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "text":
return (
<QuestionText
question={question}
questionIndex={questionIndex}
heightClassName={compactTextHeight}
disabled={disabled}
/>
);
default:
return null;
}
}
function QuestionFlowWrapper({
visibleQuestions,
itemSlug,
dobQuestion,
dobQuestionIndex,
requiredQuestionsCount,
continueLabel,
questionsListHref,
}: {
visibleQuestions: QuestionField[];
itemSlug: string;
dobQuestion?: QuestionField;
dobQuestionIndex?: number;
requiredQuestionsCount: number;
continueLabel: string;
questionsListHref: string;
}) {
const { getAnswerValue } = useQuestionAnswers();
return (
<QuestionSectionFlow
key={itemSlug}
total={requiredQuestionsCount}
continueLabel={continueLabel}
exitHref={questionsListHref}
optionalQuestionIndexes={visibleQuestions.flatMap((question, index) =>
question.required ? [] : [index],
)}
>
{visibleQuestions.map((question, questionIndex) => {
let isDisabled = false;
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const dependentQuestionIndex = visibleQuestions.findIndex(
(q) => q.title === title,
);
if (dependentQuestionIndex !== -1) {
const answer = getAnswerValue(
visibleQuestions[dependentQuestionIndex],
dependentQuestionIndex,
);
isDisabled = !values.includes(String(answer));
}
}
const answer = getAnswerValue(question, questionIndex);
const hasAnswer = hasQuestionAnswerValue(answer ?? null);
let isAnswered = hasAnswer;
if (hasAnswer) {
const isEmailQuestion =
question.title.toLowerCase().includes("email") ||
question.title.includes("ایمیل");
if (isEmailQuestion) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
isAnswered = emailRegex.test(String(answer).trim());
}
}
return (
<div
key={`${itemSlug}-${question.title}`}
data-question-required={String(question.required)}
data-question-optional={String(!question.required)}
data-question-index={questionIndex}
data-question-disabled={String(isDisabled)}
data-question-answered={String(isAnswered)}
>
{renderQuestion(
question,
questionIndex,
isDisabled,
dobQuestion,
dobQuestionIndex,
)}
</div>
);
})}
</QuestionSectionFlow>
);
}
export default function QuestionDetailClient({
closeLabel,
continueLabel,
description,
informationLabel,
itemSlug,
locale = defaultLocale,
questionsListHref,
title,
}: QuestionDetailClientProps) {
const router = useRouter();
const { dictionary: t } = useI18n();
const [isTestStarted, setIsTestStarted] = useState(false);
const { data: profile, isLoading: isProfileLoading } = useMarriageProfileQuery();
const profileGender = profile?.gender;
const age = getStoredAge();
const item = getQuestionListItemBySlug(itemSlug, locale);
const profileContext = useMemo(
() => ({
age,
gender: profileGender as MarriageGender | null | undefined,
}),
[age, profileGender],
);
const visibleQuestions = useMemo(() => {
if (!item) {
return [];
}
const hasDobQuestion = item.questions.some(
(q) => q.title === "Date of Birth" || q.title === "تاریخ تولد",
);
return item.questions
.filter((question) => {
if (
hasDobQuestion &&
(question.title === "Age" || question.title === "سن")
) {
return false;
}
return isQuestionVisibleForProfile(question, profileContext);
})
.map((question) => ({
...question,
required: isQuestionRequiredForProfile(question, profileContext),
}));
}, [item, profileContext]);
const requiredQuestionsCount = useMemo(
() => visibleQuestions.filter((q) => q.required).length,
[visibleQuestions],
);
useEffect(() => {
if (isProfileLoading) {
return;
}
if (!item || isQuestionListItemVisibleForProfile(item, profileContext)) {
return;
}
router.replace(questionsListHref);
}, [isProfileLoading, item, profileContext, questionsListHref, router]);
if (isProfileLoading && item) {
// Show component skeleton/layout structure while loading profile
} else if (!item || !isQuestionListItemVisibleForProfile(item, profileContext)) {
return null;
}
if (item && item.questions.length === 0) {
if (isTestStarted) {
const testQuestions: TestQuestion[] =
item.slug === "glasser_5_needs_test"
? [
{
id: 1,
text: "How important is financial security and long-term stability in your life?",
options: [
{ label: "Very Low", value: 1 },
{ label: "Low", value: 2 },
{ label: "Moderate", value: 3 },
{ label: "High", value: 4 },
{ label: "Very High", value: 5 },
],
},
{
id: 2,
text: "To what extent do you value personal freedom and flexibility in daily choices?",
options: [
{ label: "Very Low", value: 1 },
{ label: "Low", value: 2 },
{ label: "Moderate", value: 3 },
{ label: "High", value: 4 },
{ label: "Very High", value: 5 },
],
},
{
id: 3,
text: "How much do you seek recognition and personal achievement in your career/life?",
options: [
{ label: "Very Low", value: 1 },
{ label: "Low", value: 2 },
{ label: "Moderate", value: 3 },
{ label: "High", value: 4 },
{ label: "Very High", value: 5 },
],
},
{
id: 4,
text: "How essential is fun, humor, and playfulness in your relationship?",
options: [
{ label: "Very Low", value: 1 },
{ label: "Low", value: 2 },
{ label: "Moderate", value: 3 },
{ label: "High", value: 4 },
{ label: "Very High", value: 5 },
],
},
{
id: 5,
text: "How deep is your need for emotional connection and belonging with a partner?",
options: [
{ label: "Very Low", value: 1 },
{ label: "Low", value: 2 },
{ label: "Moderate", value: 3 },
{ label: "High", value: 4 },
{ label: "Very High", value: 5 },
],
},
]
: [
{
id: 1,
text: "How many hours do you typically spend on social media each day?",
options: [
{ label: "Between 11 and 15 Years", value: "11_15" },
{ label: "Between 16 and 25 Years", value: "16_25" },
{ label: "Between 26 and 40 Years", value: "26_40" },
{ label: "Over 40 Years", value: "over_40" },
],
},
{
id: 2,
text: "I prefer spending my free time engaging in quiet, reflective activities.",
options: [
{ label: "Strongly Agree", value: "strongly_agree" },
{ label: "Agree", value: "agree" },
{ label: "Neutral / Uncertain", value: "neutral" },
{ label: "Disagree", value: "disagree" },
],
},
{
id: 3,
text: "When making important life decisions, I rely more on logical analysis than feelings.",
options: [
{ label: "Always Logical", value: "always_logical" },
{ label: "Mostly Logical", value: "mostly_logical" },
{ label: "Balanced", value: "balanced" },
{ label: "Mostly Intuitive", value: "mostly_intuitive" },
],
},
{
id: 4,
text: "How do you handle unexpected changes to your planned routine?",
options: [
{ label: "Adapt quickly and calmly", value: "adapt_quick" },
{ label: "Need a moment to adjust", value: "need_moment" },
{ label: "Feel stressed but manage", value: "stressed" },
{ label: "Prefer strict adherence to plans", value: "strict_plans" },
],
},
{
id: 5,
text: "In social gatherings, I usually initiate conversations with new acquaintances.",
options: [
{ label: "Very True", value: "very_true" },
{ label: "Somewhat True", value: "somewhat_true" },
{ label: "Rarely True", value: "rarely_true" },
{ label: "Not True At All", value: "not_true" },
],
},
];
return (
<TestQuestionsFlow
title={item.title}
questions={testQuestions}
closeLabel={closeLabel}
informationLabel={informationLabel}
onClose={() => setIsTestStarted(false)}
/>
);
}
const bulletKey = item.slug === "glasser_5_needs_test" ? "glasser" : "personality";
const bullets = t.questions.testIntroBullets[bulletKey];
return (
<>
<PageBackground disabled />
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center gap-4">
<NavigationButton
className="shrink-0"
variant="transparent"
icon="close"
iconLabel={closeLabel}
/>
<h1 className="min-w-0 flex-1 text-center font-semibold text-white truncate">
{item.title}
</h1>
<NavigationButton
className="shrink-0"
variant="transparent"
icon="info"
iconLabel={informationLabel}
/>
</div>
</StickyHeader>
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
<TestIntroPage
title={item.title}
estimateTime={item.estimate}
description={t.questions.testIntroEstimateLabel}
bulletPoints={bullets}
disclaimerText={t.questions.testIntroDisclaimer}
startLabel={t.questions.testIntroStart}
onStart={() => {
setIsTestStarted(true);
}}
/>
</div>
</main>
</>
);
}
const dobQuestion = visibleQuestions.find(
(question) => question.title === "Date of Birth",
);
const dobQuestionIndex = visibleQuestions.findIndex(
(question) => question.title === "Date of Birth",
);
return (
<>
<PageBackground disabled />
<AnswerPaceSheet
slug={item.slug}
title={title}
description={description}
continueLabel={continueLabel}
/>
<QuestionAnswersProvider slug={item.slug} questions={visibleQuestions}>
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center gap-4">
<QuestionExitNavigationButton
className="shrink-0"
variant="transparent"
icon="close"
iconLabel={closeLabel}
/>
<h1 className="min-w-0 flex-1 text-center font-semibold text-white truncate">
{item.title}
</h1>
<NavigationButton
className="shrink-0"
variant="transparent"
icon="info"
iconLabel={informationLabel}
/>
</div>
</StickyHeader>
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
<QuestionFlowWrapper
visibleQuestions={visibleQuestions}
itemSlug={item.slug}
dobQuestion={dobQuestion}
dobQuestionIndex={dobQuestionIndex}
requiredQuestionsCount={requiredQuestionsCount}
continueLabel={continueLabel}
questionsListHref={questionsListHref}
/>
</div>
</main>
</QuestionAnswersProvider>
</>
);
}